Skip to content

feat(auth): oauth#446

Merged
HuanCheng65 merged 7 commits intodevfrom
feat/oauth
Jun 25, 2025
Merged

feat(auth): oauth#446
HuanCheng65 merged 7 commits intodevfrom
feat/oauth

Conversation

@HuanCheng65
Copy link
Contributor

No description provided.

HuanCheng65 and others added 3 commits June 23, 2025 23:25
- Add OAuth module with dynamic provider loading mechanism
- Implement OAuth service with plugin and npm package loading support
- Add OAuth types and base provider class for extensibility
- Create user OAuth connection model for account linking
- Add OAuth routes: providers list, login redirect, callback handling
- Implement user synchronization logic (existing user binding, email matching, new user creation)
- Add comprehensive unit tests for OAuth service and user login methods
- Add end-to-end tests covering full OAuth flow and error scenarios
- Include example OAuth provider implementation and documentation
- Update environment configuration with OAuth settings

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Fix unknown error type handling in OAuth service methods
- Update controller method return types from Response to void
- Add proper type guards for error instanceof Error checks
- Ensure consistent error handling patterns

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Fix cookie handling in e2e tests with proper array type checking
- Update OAuth service test mocks to use correct environment variable names
- Fix nullable field handling in OAuthUserInfo type definitions
- Update test expectations to match actual OAuth service method signatures
- Resolve private method access issues in users service tests
@coderabbitai
Copy link

coderabbitai bot commented Jun 24, 2025

Walkthrough

A comprehensive OAuth authentication system was integrated into the backend, including a new database model for user OAuth connections, a dynamic OAuth provider loading service, DTOs, controller routes, and extensive documentation. The system supports multiple OAuth providers, secure token handling, user synchronization, and is fully covered by unit and end-to-end tests. Documentation and sample environment variables were also updated.

Changes

File(s) / Path(s) Change Summary
docs/oauth-implementation-guide.md, docs/oauth.md Added detailed documentation for OAuth implementation, integration plan, configuration, flow, security, extensibility, frontend integration, and troubleshooting.
prisma/schema.prisma, src/auth/oauth/oauth.prisma, src/users/users.prisma Added UserOAuthConnection model for storing OAuth user connections; updated User model to relate to OAuth connections.
prisma/migrations/.../migration.sql Created migration to add the user_o_auth_connection table with relevant fields, indexes, and constraints.
sample.env Added commented-out OAuth-related environment variables for provider enablement, plugin paths, credentials, and frontend redirect URLs.
.gitignore Added oauth-providers/ and plugins/ directories to ignore list for sensitive OAuth provider code.
src/auth/auth.module.ts Integrated OAuthModule into the authentication module imports and exports.
src/auth/oauth/oauth.module.ts Introduced OAuthModule with dynamic provider registration and module initialization logic.
src/auth/oauth/oauth.service.ts Added OAuthService for dynamic loading, validation, and management of OAuth providers, with methods for authorization, callback, and user info retrieval.
src/auth/oauth/oauth.types.ts Defined OAuth-related interfaces, base provider class, and custom error class for type safety and extensibility.
src/users/DTO/oauth.dto.ts Added DTOs for OAuth provider listing, callback query validation, and OAuth user data.
src/users/users.controller.ts Added controller endpoints for listing OAuth providers, initiating login, and handling OAuth callbacks with error handling and token management.
src/users/users.service.ts Added OAuth login/registration logic: user synchronization, connection binding, new user creation, and helper methods for username generation and profile creation.
src/auth/oauth/oauth.service.spec.ts, src/auth/oauth/oauth.module.spec.ts, src/auth/oauth/oauth.types.spec.ts Added comprehensive unit tests for OAuth service, module, and types, including provider loading, error handling, and security validation.
src/users/users.service.spec.ts Added unit tests for OAuth login flow, covering existing/new users, error cases, and data handling.
test/user.e2e-spec.ts Added end-to-end tests for OAuth authentication flows, error handling, user binding, and token validation.

Sequence Diagram(s)

sequenceDiagram
    participant Frontend
    participant UsersController
    participant OAuthService
    participant OAuthProvider
    participant UsersService
    participant DB

    Frontend->>UsersController: GET /auth/oauth/providers
    UsersController->>OAuthService: getProvidersConfig()
    OAuthService-->>UsersController: providers list
    UsersController-->>Frontend: providers list

    Frontend->>UsersController: GET /auth/oauth/login/:providerId?state=...
    UsersController->>OAuthService: generateAuthorizationUrl(providerId, state)
    OAuthService->>OAuthProvider: getAuthorizationUrl(state)
    OAuthProvider-->>OAuthService: auth URL
    OAuthService-->>UsersController: auth URL
    UsersController-->>Frontend: HTTP 302 Redirect to provider auth URL

    Frontend->>OAuthProvider: User authenticates, provider redirects to callback
    OAuthProvider-->>Frontend: Redirect to /auth/oauth/callback/:providerId?code=...&state=...

    Frontend->>UsersController: GET /auth/oauth/callback/:providerId?code=...&state=...
    UsersController->>OAuthService: handleCallback(providerId, code, state)
    OAuthService->>OAuthProvider: handleCallback(code, state)
    OAuthProvider-->>OAuthService: access_token
    OAuthService->>OAuthProvider: getUserInfo(access_token)
    OAuthProvider-->>OAuthService: userInfo
    OAuthService-->>UsersController: userInfo
    UsersController->>UsersService: loginWithOAuth(providerId, userInfo, ...)
    UsersService->>DB: Find or create user, bind OAuth connection
    DB-->>UsersService: user record
    UsersService-->>UsersController: user DTO, session token
    UsersController-->>Frontend: Redirect to frontend success page with token, user info
Loading

Suggested reviewers

  • Nictheboy
  • andylizf

Poem

In the warren of code, OAuth hops in,
With tokens and cookies, let logins begin!
Providers abound, from plugins they grow,
Users now leap through a seamless new flow.
🥕
Tests guard the tunnels, docs light the way—
Secure, extensible, hurray for today!

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@codecov
Copy link

codecov bot commented Jun 24, 2025

Codecov Report

Attention: Patch coverage is 86.39241% with 43 lines in your changes missing coverage. Please review.

Project coverage is 88.12%. Comparing base (f204c30) to head (98cc0a8).
Report is 1 commits behind head on dev.

Files with missing lines Patch % Lines
src/auth/oauth/oauth.service.ts 84.39% 22 Missing ⚠️
src/users/users.service.ts 85.86% 13 Missing ⚠️
src/users/users.controller.ts 85.18% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #446      +/-   ##
==========================================
- Coverage   88.28%   88.12%   -0.17%     
==========================================
  Files          74       77       +3     
  Lines        3313     3629     +316     
  Branches      489      560      +71     
==========================================
+ Hits         2925     3198     +273     
- Misses        368      411      +43     
  Partials       20       20              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (13)
src/auth/oauth/oauth.module.ts (1)

21-34: Well-designed dynamic module registration.

The static register method follows NestJS dynamic module patterns correctly:

  • Accepts optional providers array
  • Creates unique provider tokens to avoid conflicts
  • Properly exports the OAuth service
  • Imports necessary ConfigModule

Consider adding error boundary handling in the initialization phase:

  async onModuleInit() {
+   try {
      await this.oauthService.initialize();
+   } catch (error) {
+     // Log initialization errors appropriately
+     throw error;
+   }
  }
src/users/DTO/oauth.dto.ts (1)

40-42: Appropriate DTO inheritance pattern.

The OAuth user DTO correctly extends the base UserDto and adds the optional email field. The nullable email type properly handles cases where OAuth providers might not provide email information.

Consider adding JSDoc comments for better documentation:

+/**
+ * OAuth user DTO that extends UserDto with email field
+ * Email can be null if OAuth provider doesn't provide it
+ */
export class OAuthUserDto extends UserDto {
  email?: string | null;
}
src/auth/oauth/oauth.prisma (1)

6-11: Use English comments for consistency.

Replace Chinese comments with English equivalents to maintain consistency across the codebase.

-  providerId     String    @map("provider_id")        // OAuth提供商ID,如 'ruc', 'google'
-  providerUserId String    @map("provider_user_id")  // 提供商侧用户唯一标识
+  providerId     String    @map("provider_id")        // OAuth provider ID, e.g., 'ruc', 'google'
+  providerUserId String    @map("provider_user_id")  // User's unique identifier from provider
   /// [rawProfileType]
-  rawProfile     Json?     @map("raw_profile")       // 原始用户信息(JSON)
-  refreshToken   String?   @map("refresh_token")     // 可选,OAuth长效令牌
-  tokenExpires   DateTime? @map("token_expires")     // 可选,OAuth令牌过期时间
+  rawProfile     Json?     @map("raw_profile")       // Raw user info from provider (JSON)
+  refreshToken   String?   @map("refresh_token")     // Optional, OAuth refresh token
+  tokenExpires   DateTime? @map("token_expires")     // Optional, OAuth token expiration time
plugins/oauth/example.js (3)

14-16: Use English comments for consistency.

Replace Chinese comments with English equivalents.

-      authorizationUrl: 'https://example.com/oauth/authorize', // 替换为实际的授权 URL
-      tokenUrl: 'https://example.com/oauth/token',             // 替换为实际的 token URL
-      scope: ['read:user', 'user:email'],                     // 替换为实际需要的权限
+      authorizationUrl: 'https://example.com/oauth/authorize', // Replace with actual authorization URL
+      tokenUrl: 'https://example.com/oauth/token',             // Replace with actual token URL
+      scope: ['read:user', 'user:email'],                     // Replace with required scopes

58-62: Use optional chaining for safer access.

Improve null safety by using optional chaining as suggested by static analysis.

-      if (response.data && response.data.access_token) {
-        return response.data.access_token;
+      if (response.data?.access_token) {
+        return response.data.access_token;

70-70: Use English comments for URL placeholders.

-      const response = await axios.get('https://example.com/api/user', { // 替换为实际的用户信息 API
+      const response = await axios.get('https://example.com/api/user', { // Replace with actual user info API

Also applies to: 79-79

prisma/schema.prisma (1)

202-207: Use English comments for consistency.

-  providerId     String    @map("provider_id") // OAuth提供商ID,如 'ruc', 'google'
-  providerUserId String    @map("provider_user_id") // 提供商侧用户唯一标识
+  providerId     String    @map("provider_id") // OAuth provider ID, e.g., 'ruc', 'google'
+  providerUserId String    @map("provider_user_id") // User's unique identifier from provider
   /// [rawProfileType]
-  rawProfile     Json?     @map("raw_profile") // 原始用户信息(JSON)
-  refreshToken   String?   @map("refresh_token") // 可选,OAuth长效令牌
-  tokenExpires   DateTime? @map("token_expires") // 可选,OAuth令牌过期时间
+  rawProfile     Json?     @map("raw_profile") // Raw user info from provider (JSON)
+  refreshToken   String?   @map("refresh_token") // Optional, OAuth refresh token
+  tokenExpires   DateTime? @map("token_expires") // Optional, OAuth token expiration time
src/users/users.service.ts (1)

741-763: Consider using optional chaining for cleaner code.

The method correctly handles placeholder emails for OAuth users. However, the conditional check can be simplified.

Apply this diff to use optional chaining:

-    // 检查是否是占位符email,如果是则返回null
-    const email =
-      user.email && user.email.endsWith('@placeholder.internal')
-        ? null
-        : user.email;
+    // 检查是否是占位符email,如果是则返回null
+    const email = user.email?.endsWith('@placeholder.internal')
+      ? null
+      : user.email;
src/auth/oauth/oauth.service.ts (1)

197-221: Consider making the npm package namespace configurable.

The npm package namespace is currently hardcoded to @sageseekersociety/cheese-auth-. For better flexibility across different deployments or organizations, consider making this configurable.

Add a configuration option for the npm namespace:

   private async tryLoadFromNpm(
     providerId: string,
     config: OAuthProviderConfig,
   ): Promise<OAuthProvider | null> {
-    const packageName = `@sageseekersociety/cheese-auth-${providerId}-oauth-provider`;
+    const npmNamespace = this.configService.get<string>('OAUTH_NPM_NAMESPACE') || '@sageseekersociety/cheese-auth';
+    const packageName = `${npmNamespace}-${providerId}-oauth-provider`;
test/user.e2e-spec.ts (1)

1394-1416: Remove debug console.log statements from tests.

Debug logging statements should be removed before merging to keep test output clean.

Remove the console.log statements:

-      // Add debug logging to verify email matching
-      console.log(`Test: regularUser email = "${regularUser.email}"`);
-      console.log(`Test: regularUser userId = ${regularUser.userId}`);
-
       // Override OAuth mocks to return the regularUser's email
       const oauthService = app.get(OAuthService);
       if (oauthService) {
         jest
           .spyOn(oauthService, 'handleCallback')
           .mockResolvedValue('mock_access_token_existing');
         jest.spyOn(oauthService, 'getUserInfo').mockImplementation(async () => {
           const userInfo = {
             id: `oauth-binding-test-${Date.now()}`, // Use unique ID to avoid conflicts
             email: regularUser.email, // Use the exact email from regularUser
             name: 'OAuth Existing User',
             username: 'oauthexisting',
             preferredUsername: 'oauthexisting',
           };
-          console.log(
-            `Mock: OAuth getUserInfo returning email = "${userInfo.email}"`,
-          );
           return userInfo;
         });
       }
src/users/users.controller.ts (1)

1261-1348: Well-implemented OAuth callback handler

The OAuth callback implementation is comprehensive with proper error handling, logging, and security considerations. The cookie configuration with sameSite: 'lax' is appropriate for OAuth flows.

Consider documenting that the JWT token is passed via URL query parameter (line 1318) for security-conscious deployments. While this is a common pattern for SPAs, some organizations may prefer alternative approaches like temporary server-side storage with a one-time code exchange.

src/auth/oauth/oauth.types.ts (1)

1-6: Update author attribution

Please replace "Claude Assistant" with the actual contributor's name and email.

-* Author(s):
-*     Claude Assistant
+* Author(s):
+*     Your Name <your.email@example.com>
docs/oauth.md (1)

1-5: Clean up informal introduction

The beginning of the documentation reads like conversation notes rather than formal documentation. Consider removing or rewriting the informal introduction.

-明白了。我将研究如何将cheese-auth仓库中的OAuth认证功能(特别是你们学校的OAuth登录支持)作为可选的登录方式整合回cheese-backend仓库中,并确保其不会破坏现有的认证逻辑。
-
-我会仔细分析cheese-auth中OAuth相关的模块、依赖和接口,评估其如何在cheese-backend中以模块化方式集成,包括必要的中间件、配置、用户信息同步等部分。完成后会整理成一份清晰的迁移方案供你参考。
-
-
+# OAuth Integration Guide for Cheese-Backend
+
+This document describes how to integrate OAuth authentication functionality from the cheese-auth repository into cheese-backend as an optional login method, while preserving existing authentication flows.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f204c30 and baa752c.

📒 Files selected for processing (17)
  • docs/oauth-implementation-guide.md (1 hunks)
  • docs/oauth.md (1 hunks)
  • plugins/oauth/example.js (1 hunks)
  • prisma/schema.prisma (2 hunks)
  • sample.env (1 hunks)
  • src/auth/auth.module.ts (2 hunks)
  • src/auth/oauth/oauth.module.ts (1 hunks)
  • src/auth/oauth/oauth.prisma (1 hunks)
  • src/auth/oauth/oauth.service.spec.ts (1 hunks)
  • src/auth/oauth/oauth.service.ts (1 hunks)
  • src/auth/oauth/oauth.types.ts (1 hunks)
  • src/users/DTO/oauth.dto.ts (1 hunks)
  • src/users/users.controller.ts (6 hunks)
  • src/users/users.prisma (2 hunks)
  • src/users/users.service.spec.ts (1 hunks)
  • src/users/users.service.ts (4 hunks)
  • test/user.e2e-spec.ts (7 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
plugins/oauth/example.js

[error] 59-60: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/users/users.service.ts

[error] 759-764: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 1707-1708: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🪛 LanguageTool
docs/oauth.md

[uncategorized] ~91-~91: 您的意思是“"不"建”?
Context: ...录存在且用户存在,但发现用户缺少用户档案(profile)(理论上不应发生),则补建默认档案以保持数据一致性。 2. 按邮箱匹配现有用户: 如果没有现成...

(BU)


[uncategorized] ~95-~95: 您的意思是“"账"号”?
Context: ...,update 分支会更新绑定到当前用户ID。这一操作将日志记录输出,表示发生了帐号关联。 * 之后复用找到的本地用户记录,跳至步骤4。 3. **创建新...

(ZHANG7_ZHANG8)


[uncategorized] ~100-~100: 数词与名词之间一般应存在量词,可能缺少量词。
Context: ...ID}`。规范化该用户名:去除特殊字符、转为小写,并确保长度在合适范围内(如不足4字符则补前缀,超长则截断)。然后检查是否与现有用户重名,若是则在末尾追加递增数字后缀...

(wa5)


[uncategorized] ~100-~100: 您的意思是“"不"前缀”?
Context: ...。规范化该用户名:去除特殊字符、转为小写,并确保长度在合适范围内(如不足4字符则补前缀,超长则截断)。然后检查是否与现有用户重名,若是则在末尾追加递增数字后缀确保...

(BU)


[uncategorized] ~134-~134: 动词的修饰一般为‘形容词(副词)+地+动词’。您的意思是否是:相同"地"会话
Context: ...in 方法所做的记录和 token 发放逻辑),将 OAuth 登录结果接入相同的会话管理流程,实现统一的用户体验。 ## 模块集成与兼容性 有了上述组件,实...

(wb4)


[uncategorized] ~147-~147: 您的意思是“"既"是”吗?
Context: ...sController 构造函数即可通过 DI 拿到它。Cheese-Auth 即是在 UsersController 中注入了 OAuthService,因此我...

(JI11_JI2)


[uncategorized] ~147-~147: 动词的修饰一般为‘形容词(副词)+地+动词’。您的意思是否是:相同"地"依赖
Context: ...ersController 中注入了 OAuthService,因此我们保持相同的依赖关系即可。 * 路由集成: 将上文列出的 `/users/auth...

(wb4)


[uncategorized] ~155-~155: 您的意思是“"不"验证”?
Context: ...ontroller 增加调用它的入口。因此,原有邮箱验证、密码重置、TOTP 两步验证等功能都与 OAuth 登录互不冲突,各自按需执行。 * **前端配合:...

(BU)


[uncategorized] ~187-~187: 能愿动词不能成为‘把’字句、‘被’字句的谓语动词。应该是:"便于把……实现"。
Context: ...现作为私有文件部署(通过文档指导运维放置),而不直接提交代码。当然,这需要权衡:把实现代码纳入仓库便于版本管理,但需确保不包含敏感信息(比如具体的学校OAuth端点可能是公开协议,无...

(wa3)

⏰ Context from checks skipped due to timeout of 90000ms (11)
  • GitHub Check: test (16, 8.0.0, 20.x)
  • GitHub Check: build-and-push-image
  • GitHub Check: test (latest, 8.0.0, 22.x)
  • GitHub Check: test (16, 8.0.0, 22.x)
  • GitHub Check: test (16, 8.12.2, 20.x)
  • GitHub Check: test (latest, 8.0.0, 20.x)
  • GitHub Check: test (latest, 8.12.2, 22.x)
  • GitHub Check: test (16, 8.12.2, 22.x)
  • GitHub Check: test (latest, 8.12.2, 20.x)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: test (latest, 8.12.2, 22.x)
🔇 Additional comments (15)
sample.env (1)

90-108: Comprehensive OAuth configuration setup.

The OAuth environment variables are well-structured and cover all necessary aspects:

  • Provider management (enabled providers, plugin paths)
  • Security settings (npm loading control)
  • Provider credentials with clear naming convention
  • Frontend integration paths

All sensitive values are appropriately commented out for a sample file.

src/auth/auth.module.ts (3)

16-16: Proper OAuth module integration.

The import statement correctly brings in the OAuth module for integration.


34-34: Dynamic OAuth module registration.

Using OAuthModule.register() follows NestJS dynamic module patterns and allows for flexible OAuth provider configuration.


38-38: OAuth module properly exported.

Exporting the OAuth module makes it available to other modules that import AuthModule, following good architectural practices.

src/users/users.prisma (2)

12-12: Correct import for OAuth connection model.

The import statement properly references the OAuth connection model from the auth module.


58-58: Proper one-to-many OAuth relationship.

The oauthConnections field correctly establishes a one-to-many relationship between users and their OAuth connections, allowing multiple OAuth providers per user.

src/auth/oauth/oauth.module.ts (1)

14-19: Proper lifecycle management for OAuth initialization.

The OnModuleInit implementation ensures OAuth service is properly initialized when the module starts. The async initialization allows for proper setup of OAuth providers.

src/users/DTO/oauth.dto.ts (2)

12-20: Well-structured OAuth providers response DTO.

The DTO properly extends the base response pattern and defines the expected structure for OAuth provider information including id, name, and scope arrays.


22-37: Comprehensive OAuth callback validation.

The DTO correctly handles OAuth 2.0 standard callback parameters:

  • Required code parameter for authorization code flow
  • Optional state for CSRF protection
  • Optional error parameters for error handling

All fields are properly validated with class-validator decorators.

src/users/users.service.ts (2)

44-44: LGTM! OAuth type imports added correctly.

The OAuth-related type imports are properly organized and follow the existing import structure.

Also applies to: 54-54


1858-1945: Well-designed OAuth helper methods!

The helper methods provide robust support for OAuth user creation:

  • Smart username generation with multiple fallbacks
  • Proper uniqueness handling with counter suffixes
  • Secure random password generation
  • Default profile creation for edge cases

Great implementation!

docs/oauth-implementation-guide.md (1)

1-286: Excellent OAuth implementation documentation!

The guide provides comprehensive coverage of:

  • Clear architectural overview
  • Step-by-step implementation instructions
  • Security best practices
  • Troubleshooting guidance

The documentation will be invaluable for developers implementing new OAuth providers.

src/auth/oauth/oauth.service.ts (1)

8-332: Excellent OAuth service implementation!

The service demonstrates:

  • Robust security with path traversal protection
  • Comprehensive error handling and logging
  • Clean modular design for provider management
  • Proper async initialization pattern

Great architectural design for extensible OAuth support!

test/user.e2e-spec.ts (2)

16-18: Good OAuth test setup!

The OAuth-related imports and environment variable configuration before module initialization ensure proper test isolation and configuration.

Also applies to: 225-229


1212-1545: Comprehensive OAuth e2e test coverage!

The OAuth test suite thoroughly covers:

  • All OAuth endpoints and flows
  • Success and error scenarios
  • User binding and creation logic
  • Token validation and cookie handling

Excellent test implementation that ensures the OAuth feature works correctly end-to-end!

Comment on lines +45 to +56
const response = await axios.post(this.config.tokenUrl, {
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
code,
grant_type: 'authorization_code',
redirect_uri: this.config.redirectUrl,
}, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix Content-Type header to match data format.

The code sends JSON data but sets Content-Type to application/x-www-form-urlencoded. This mismatch could cause OAuth providers to reject the request.

-      const response = await axios.post(this.config.tokenUrl, {
-        client_id: this.config.clientId,
-        client_secret: this.config.clientSecret,
-        code,
-        grant_type: 'authorization_code',
-        redirect_uri: this.config.redirectUrl,
-      }, {
-        headers: {
-          'Accept': 'application/json',
-          'Content-Type': 'application/x-www-form-urlencoded',
-        },
-      });
+      const params = new URLSearchParams({
+        client_id: this.config.clientId,
+        client_secret: this.config.clientSecret,
+        code,
+        grant_type: 'authorization_code',
+        redirect_uri: this.config.redirectUrl,
+      });
+      
+      const response = await axios.post(this.config.tokenUrl, params.toString(), {
+        headers: {
+          'Accept': 'application/json',
+          'Content-Type': 'application/x-www-form-urlencoded',
+        },
+      });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await axios.post(this.config.tokenUrl, {
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
code,
grant_type: 'authorization_code',
redirect_uri: this.config.redirectUrl,
}, {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
});
const params = new URLSearchParams({
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
code,
grant_type: 'authorization_code',
redirect_uri: this.config.redirectUrl,
});
const response = await axios.post(this.config.tokenUrl, params.toString(), {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
});
🤖 Prompt for AI Agents
In plugins/oauth/example.js around lines 45 to 56, the Content-Type header is
set to 'application/x-www-form-urlencoded' while the data is sent as JSON,
causing a mismatch. To fix this, either change the Content-Type header to
'application/json' to match the JSON data format or convert the data to
URL-encoded format to match the existing header. Ensure the header and data
format are consistent for the OAuth provider to accept the request.

```typescript
// plugins/oauth/your-provider.ts
import axios from 'axios';
import { BaseOAuthProvider, OAuthProviderConfig, OAuthUserInfo } from '../../src/auth/oauth/oauth.types';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Clarify the import path for TypeScript providers.

The import path assumes the provider is located relative to the src directory, which may not be accurate for plugins.

Consider updating the documentation to clarify the correct import approach:

-import { BaseOAuthProvider, OAuthProviderConfig, OAuthUserInfo } from '../../src/auth/oauth/oauth.types';
+// Note: Adjust the import path based on your provider location
+// For plugins in plugins/oauth/, you may need to use a different path or
+// have these types available as a separate package
+import { BaseOAuthProvider, OAuthProviderConfig, OAuthUserInfo } from '@your-app/oauth-types';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { BaseOAuthProvider, OAuthProviderConfig, OAuthUserInfo } from '../../src/auth/oauth/oauth.types';
// Note: Adjust the import path based on your provider location
// For plugins in plugins/oauth/, you may need to use a different path or
// have these types available as a separate package
import { BaseOAuthProvider, OAuthProviderConfig, OAuthUserInfo } from '@your-app/oauth-types';
🤖 Prompt for AI Agents
In docs/oauth-implementation-guide.md at line 129, clarify the import path for
TypeScript providers by explaining that the relative import from
'../../src/auth/oauth/oauth.types' assumes the provider is within the src
directory, which may not be correct for plugins. Update the documentation to
guide users on adjusting the import path based on their project structure or
plugin location to avoid confusion.

Comment on lines +89 to +114
async handleCallback(code, state) {
const response = await axios.post(this.config.tokenUrl, {
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
code,
grant_type: 'authorization_code',
redirect_uri: this.config.redirectUrl,
});

return response.data.access_token;
}

async getUserInfo(accessToken) {
const response = await axios.get('https://your-provider.com/api/user', {
headers: { 'Authorization': `Bearer ${accessToken}` },
});

const userData = response.data;
return {
id: userData.id.toString(),
email: userData.email,
name: userData.name,
username: userData.username,
preferredUsername: userData.preferred_username,
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling to the provider example.

The example provider implementation should demonstrate proper error handling for production use.

Consider wrapping axios calls in try-catch blocks:

   async handleCallback(code, state) {
+    try {
       const response = await axios.post(this.config.tokenUrl, {
         client_id: this.config.clientId,
         client_secret: this.config.clientSecret,
         code,
         grant_type: 'authorization_code',
         redirect_uri: this.config.redirectUrl,
       });
 
       return response.data.access_token;
+    } catch (error) {
+      throw new Error(`Failed to exchange code for token: ${error.message}`);
+    }
   }

   async getUserInfo(accessToken) {
+    try {
       const response = await axios.get('https://your-provider.com/api/user', {
         headers: { 'Authorization': `Bearer ${accessToken}` },
       });
 
       const userData = response.data;
       return {
         id: userData.id.toString(),
         email: userData.email,
         name: userData.name,
         username: userData.username,
         preferredUsername: userData.preferred_username,
       };
+    } catch (error) {
+      throw new Error(`Failed to fetch user info: ${error.message}`);
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async handleCallback(code, state) {
const response = await axios.post(this.config.tokenUrl, {
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
code,
grant_type: 'authorization_code',
redirect_uri: this.config.redirectUrl,
});
return response.data.access_token;
}
async getUserInfo(accessToken) {
const response = await axios.get('https://your-provider.com/api/user', {
headers: { 'Authorization': `Bearer ${accessToken}` },
});
const userData = response.data;
return {
id: userData.id.toString(),
email: userData.email,
name: userData.name,
username: userData.username,
preferredUsername: userData.preferred_username,
};
}
async handleCallback(code, state) {
try {
const response = await axios.post(this.config.tokenUrl, {
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
code,
grant_type: 'authorization_code',
redirect_uri: this.config.redirectUrl,
});
return response.data.access_token;
} catch (error) {
throw new Error(`Failed to exchange code for token: ${error.message}`);
}
}
async getUserInfo(accessToken) {
try {
const response = await axios.get('https://your-provider.com/api/user', {
headers: { 'Authorization': `Bearer ${accessToken}` },
});
const userData = response.data;
return {
id: userData.id.toString(),
email: userData.email,
name: userData.name,
username: userData.username,
preferredUsername: userData.preferred_username,
};
} catch (error) {
throw new Error(`Failed to fetch user info: ${error.message}`);
}
}
🤖 Prompt for AI Agents
In docs/oauth-implementation-guide.md around lines 89 to 114, the axios calls in
handleCallback and getUserInfo lack error handling. Wrap each axios call in a
try-catch block to catch and handle errors gracefully. In the catch block, log
or throw a meaningful error message to ensure the provider implementation
demonstrates proper error handling for production use.

Comment on lines +1238 to +1239
@Res() res?: Response,
): Promise<void> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove optional modifier from Response parameter

The res parameter should not be optional when using @Res() decorator. NestJS always provides the Response object when this decorator is used.

-    @Res() res?: Response,
+    @Res() res: Response,

Also update line 1246 to remove the non-null assertion:

-      res!.redirect(authUrl);
+      res.redirect(authUrl);

And line 1254:

-        res!.redirect(errorUrl);
+        res.redirect(errorUrl);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Res() res?: Response,
): Promise<void> {
@Get('auth/oauth/login/:providerId')
@NoAuth()
async oauthLogin(
@Req() req: Request,
- @Res() res?: Response,
+ @Res() res: Response,
): Promise<void> {
const { providerId } = req.params;
let authUrl: string;
try {
authUrl = this.oauthService.getAuthorizationUrl(providerId);
- res!.redirect(authUrl);
+ res.redirect(authUrl);
} catch (err) {
const errorUrl = this.oauthService.getErrorRedirectUrl(providerId, err);
- res!.redirect(errorUrl);
+ res.redirect(errorUrl);
}
}
🤖 Prompt for AI Agents
In src/users/users.controller.ts around lines 1238 to 1239, remove the optional
modifier from the `res` parameter so it is declared as `@Res() res: Response`
without the question mark. Then, on line 1246, remove the non-null assertion
operator (!) when accessing `res`. Also, update line 1254 to remove any non-null
assertions related to `res`. This ensures the Response object is treated as
always present, consistent with NestJS behavior.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/users/users.service.spec.ts (1)

4-6: Remove AI assistant attribution.

Remove the author attribution referencing an AI assistant.

  * Author(s):
- *      HuanCheng65
+ *      [Your name or team]
src/auth/oauth/oauth.service.spec.ts (1)

4-6: Remove AI assistant attribution.

Remove the author attribution referencing an AI assistant.

  * Author(s):
- *      HuanCheng65
+ *      [Your name or team]
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between baa752c and bbf2c58.

⛔ Files ignored due to path filters (1)
  • prisma/migrations/migration_lock.toml is excluded by !**/*.toml
📒 Files selected for processing (10)
  • .gitignore (1 hunks)
  • prisma/migrations/20250625052726_add_oauth_tables/migration.sql (1 hunks)
  • src/auth/oauth/oauth.module.spec.ts (1 hunks)
  • src/auth/oauth/oauth.module.ts (1 hunks)
  • src/auth/oauth/oauth.service.spec.ts (1 hunks)
  • src/auth/oauth/oauth.service.ts (1 hunks)
  • src/auth/oauth/oauth.types.spec.ts (1 hunks)
  • src/auth/oauth/oauth.types.ts (1 hunks)
  • src/users/DTO/oauth.dto.ts (1 hunks)
  • src/users/users.service.spec.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • .gitignore
  • prisma/migrations/20250625052726_add_oauth_tables/migration.sql
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/auth/oauth/oauth.module.ts
  • src/users/DTO/oauth.dto.ts
  • src/auth/oauth/oauth.service.ts
  • src/auth/oauth/oauth.types.ts
🧰 Additional context used
🪛 Biome (1.9.4)
src/auth/oauth/oauth.types.spec.ts

[error] 17-19: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.

(lint/complexity/noUselessConstructor)

⏰ Context from checks skipped due to timeout of 90000ms (19)
  • GitHub Check: test (16, 8.0.0, 20.x)
  • GitHub Check: test (16, 8.12.2, 20.x)
  • GitHub Check: test (latest, 8.0.0, 20.x)
  • GitHub Check: test (16, 8.12.2, 22.x)
  • GitHub Check: test (latest, 8.12.2, 20.x)
  • GitHub Check: test (latest, 8.0.0, 22.x)
  • GitHub Check: test (latest, 8.12.2, 22.x)
  • GitHub Check: build-and-push-image
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: test (latest, 8.12.2, 22.x)
  • GitHub Check: test (latest, 8.12.2, 22.x)
  • GitHub Check: test (latest, 8.0.0, 22.x)
  • GitHub Check: test (16, 8.12.2, 22.x)
  • GitHub Check: test (16, 8.0.0, 20.x)
  • GitHub Check: test (16, 8.0.0, 22.x)
  • GitHub Check: test (16, 8.12.2, 20.x)
  • GitHub Check: test (latest, 8.0.0, 20.x)
  • GitHub Check: test (latest, 8.12.2, 22.x)
  • GitHub Check: test (latest, 8.12.2, 20.x)
🔇 Additional comments (5)
src/auth/oauth/oauth.module.spec.ts (1)

13-194: Well-structured and comprehensive test coverage!

The test suite thoroughly covers the OAuthModule functionality including dynamic registration, lifecycle hooks, and various edge cases. The mocking strategy is appropriate and the test organization follows best practices.

src/auth/oauth/oauth.types.spec.ts (1)

42-245: Excellent test coverage for OAuth types!

The tests comprehensively cover:

  • Authorization URL generation with various parameter combinations
  • Proper URL encoding and special character handling
  • Error class functionality and inheritance
  • Edge cases like empty scopes

The test organization and naming conventions are clear and follow best practices.

src/users/users.service.spec.ts (1)

131-731: Outstanding test coverage for OAuth login functionality!

This test suite is exemplary with:

  • Comprehensive scenario coverage (13+ test cases)
  • Proper edge case handling (deleted users, missing data, special characters)
  • Transaction and error handling verification
  • Security considerations (XSS prevention, data sanitization)
  • Clear test organization and descriptive test names

The mocking strategy effectively isolates the service under test while verifying the correct interactions with dependencies.

src/auth/oauth/oauth.service.spec.ts (2)

764-859: Excellent security validation tests!

The security test suite demonstrates thorough attention to security concerns:

  • Path traversal prevention in plugin loading
  • XSS attempt filtering in provider IDs
  • Command injection prevention
  • Proper validation and sanitization of user inputs

This security-first approach is commendable and ensures the OAuth implementation is resilient against common attack vectors.


67-701: Comprehensive test coverage for OAuthService!

The test suite thoroughly covers:

  • All service methods with both success and error scenarios
  • Dynamic provider loading from plugins and NPM packages
  • Configuration validation and error handling
  • Edge cases like multiple initialization attempts
  • Proper isolation with filesystem and module mocking

The test organization follows a clear structure making it easy to understand and maintain.

Comment on lines +17 to +19
constructor(config: OAuthProviderConfig) {
super(config);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Remove the unnecessary constructor.

The constructor only calls super(config) without any additional logic. This is automatically inherited from the parent class.

Apply this diff to remove the unnecessary constructor:

 class TestOAuthProvider extends BaseOAuthProvider {
-  constructor(config: OAuthProviderConfig) {
-    super(config);
-  }
-
   async handleCallback(code: string, state?: string): Promise<string> {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor(config: OAuthProviderConfig) {
super(config);
}
class TestOAuthProvider extends BaseOAuthProvider {
async handleCallback(code: string, state?: string): Promise<string> {
// ... existing implementation ...
}
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 17-19: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.

(lint/complexity/noUselessConstructor)

🤖 Prompt for AI Agents
In src/auth/oauth/oauth.types.spec.ts around lines 17 to 19, the constructor is
redundant as it only calls super(config) without adding any logic. Remove the
entire constructor method to rely on the parent class's constructor inheritance.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/auth/oauth/oauth.service.spec.ts (2)

256-464: Thorough testing of core OAuth operations.

The tests for getProvider, generateAuthorizationUrl, handleCallback, and getUserInfo properly cover both success and error scenarios. The consistent error handling for nonexistent providers is well-implemented.

Consider refactoring duplicate setup code.

There's significant duplication in the beforeEach blocks across these test suites. Consider extracting common mock setup into helper functions.


880-892: Address static analysis suggestion carefully.

The static analysis tool suggests using optional chaining on line 887, but the current explicit null checking is actually safer in this context:

if (pathStr && pathStr.includes('test')) {

This pattern correctly checks for both null/undefined and ensures pathStr is truthy before calling methods on it. Optional chaining would be pathStr?.includes('test') but this would return undefined instead of false for null/undefined values, which could change the conditional logic.

The current code is correct and safer than the suggested optional chaining in this specific case.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 669a050 and 98cc0a8.

⛔ Files ignored due to path filters (3)
  • package.json is excluded by !**/*.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/*.yaml
  • tsconfig.build.json is excluded by !**/*.json
📒 Files selected for processing (2)
  • .gitignore (1 hunks)
  • src/auth/oauth/oauth.service.spec.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .gitignore
🧰 Additional context used
🪛 Biome (1.9.4)
src/auth/oauth/oauth.service.spec.ts

[error] 887-887: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (20)
  • GitHub Check: test (latest, 8.12.2, 20.x)
  • GitHub Check: test (16, 8.12.2, 20.x)
  • GitHub Check: test (latest, 8.0.0, 22.x)
  • GitHub Check: test (16, 8.0.0, 22.x)
  • GitHub Check: test (latest, 8.12.2, 22.x)
  • GitHub Check: test (latest, 8.0.0, 20.x)
  • GitHub Check: test (16, 8.12.2, 22.x)
  • GitHub Check: test (16, 8.0.0, 20.x)
  • GitHub Check: build-and-push-image
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: test (16, 8.0.0, 22.x)
  • GitHub Check: test (16, 8.0.0, 20.x)
  • GitHub Check: test (latest, 8.0.0, 20.x)
  • GitHub Check: test (latest, 8.12.2, 22.x)
  • GitHub Check: test (16, 8.12.2, 20.x)
  • GitHub Check: test (latest, 8.0.0, 22.x)
  • GitHub Check: test (latest, 8.12.2, 20.x)
  • GitHub Check: test (16, 8.12.2, 22.x)
  • GitHub Check: test (latest, 8.12.2, 22.x)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
src/auth/oauth/oauth.service.spec.ts (6)

1-23: Well-structured test file setup.

The file header, imports, and initial mocking setup are properly organized. The fs module mocking is correctly placed before the import to ensure proper test isolation.


24-65: Excellent mock provider implementation.

The MockOAuthProvider properly implements the OAuthProvider interface with realistic OAuth flow simulation. The conditional logic for valid/invalid codes and tokens provides good test coverage scenarios.


72-131: Comprehensive test setup with proper isolation.

The test module setup correctly mocks all dependencies and the cleanup in afterEach ensures proper test isolation by resetting service state. This is essential for preventing test interference in a stateful service.


132-254: Comprehensive initialization test coverage.

The initialization tests thoroughly cover various scenarios including error handling, security validation, and edge cases. The provider ID validation tests are particularly important for preventing security vulnerabilities.


764-908: Excellent security validation coverage.

The security tests comprehensively cover path traversal, XSS, and injection attack prevention. These tests are crucial for a service that dynamically loads OAuth providers from external files and npm packages. The validation of provider IDs and plugin paths helps prevent security vulnerabilities.


1406-1591: Comprehensive edge case and configuration handling.

The remaining tests provide excellent coverage of configuration parsing, error handling, and internal method testing. The tests for non-Error objects being thrown and whitespace handling in configuration are particularly valuable for production robustness.

Excellent overall test suite.

This test file demonstrates exceptional testing practices with comprehensive coverage of functionality, security, error handling, and edge cases. The OAuth service is thoroughly validated across all scenarios.

@HuanCheng65 HuanCheng65 merged commit b93af80 into dev Jun 25, 2025
23 of 25 checks passed
@HuanCheng65 HuanCheng65 deleted the feat/oauth branch June 25, 2025 07:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant